﻿@using Microsoft.JSInterop
@using BasicTestApp.InteropTest
@using System.Runtime.InteropServices
@using System.Text.Json
@using System.IO
@using static BasicTestApp.InteropTest.JSStreamReferenceInterop
@inject IJSRuntime JSRuntime

<button id="btn-interop" @onclick="InvokeInteropAsync">Invoke interop!</button>

<div>
    <h1>Invocations</h1>
    @foreach (var invocation in Invocations)
    {
        <h2>@invocation.Key</h2>
        <p id="@invocation.Key">@invocation.Value</p>
    }
</div>

<div>
    <h1>.NET to JS calls: passing .NET object by ref, receiving .NET object by ref</h1>
    @foreach (var kvp in ReceiveDotNetObjectByRefResult)
    {
        <h2>@(kvp.Key)Sync</h2>
        <p id="@(kvp.Key)Sync">@kvp.Value</p>
    }
    @foreach (var kvp in ReceiveDotNetObjectByRefAsyncResult)
    {
        <h2>@(kvp.Key)Async</h2>
        <p id="@(kvp.Key)Async">@kvp.Value</p>
    }
</div>

<div>
    <h1>Return values and exceptions thrown from .NET</h1>
    @foreach (var returnValue in ReturnValues)
    {
        <h2>@returnValue.Key</h2>
        <p id="@returnValue.Key">@returnValue.Value</p>
    }
</div>

<div>
    <h1>Exceptions thrown from JavaScript</h1>
    <h2>@nameof(ExceptionFromSyncMethod)</h2>
    <p id="@nameof(ExceptionFromSyncMethod)">@ExceptionFromSyncMethod?.Message</p>
    <h2>@nameof(SyncExceptionFromAsyncMethod)</h2>
    <p id="@nameof(SyncExceptionFromAsyncMethod)">@SyncExceptionFromAsyncMethod?.Message</p>
    <h2>@nameof(AsyncExceptionFromAsyncMethod)</h2>
    <p id="@nameof(AsyncExceptionFromAsyncMethod)">@AsyncExceptionFromAsyncMethod?.Message</p>
    <h2>@nameof(JSObjectReferenceInvokeNonFunctionException)</h2>
    <p id="@nameof(JSObjectReferenceInvokeNonFunctionException)">@JSObjectReferenceInvokeNonFunctionException?.Message</p>
</div>
@if (DoneWithInterop)
{
    <p id="done-with-interop">Done with interop.</p>
}

@code {
    public IDictionary<string, string> ReturnValues { get; set; } = new Dictionary<string, string>();
    public IDictionary<string, string> Invocations { get; set; } = new Dictionary<string, string>();

    public JSException ExceptionFromSyncMethod { get; set; }
    public JSException SyncExceptionFromAsyncMethod { get; set; }
    public JSException AsyncExceptionFromAsyncMethod { get; set; }
    public JSException JSObjectReferenceInvokeNonFunctionException { get; set; }

    public IDictionary<string, object> ReceiveDotNetObjectByRefResult { get; set; } = new Dictionary<string, object>();
    public IDictionary<string, object> ReceiveDotNetObjectByRefAsyncResult { get; set; } = new Dictionary<string, object>();

    public bool DoneWithInterop { get; set; }

    public async Task InvokeInteropAsync()
    {
        var shouldSupportSyncInterop = RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"));
        var testDTOTOPassByRef = new TestDTO(nonSerializedValue: 123);
        var instanceMethodsTarget = new JavaScriptInterop();
        var genericType = new JavaScriptInterop.GenericType<string> { Value = "Initial value" };

        Console.WriteLine("Starting interop invocations.");
        await JSRuntime.InvokeVoidAsync(
            "jsInteropTests.invokeDotNetInteropMethodsAsync",
            shouldSupportSyncInterop,
            DotNetObjectReference.Create(testDTOTOPassByRef),
            DotNetObjectReference.Create(instanceMethodsTarget),
            DotNetObjectReference.Create(genericType));

        if (shouldSupportSyncInterop)
        {
            InvokeInProcessInterop();
        }

        Console.WriteLine("Showing interop invocation results.");
        var collectResults = await JSRuntime.InvokeAsync<Dictionary<string, string>>("jsInteropTests.collectInteropResults");

        ReturnValues = collectResults.ToDictionary(kvp => kvp.Key, kvp => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(kvp.Value)));

        var invocations = new Dictionary<string, string>();
        foreach (var interopResult in JavaScriptInterop.Invocations)
        {
            var interopResultValue = JsonSerializer.Serialize(interopResult.Value, TestJsonSerializerOptionsProvider.Options);
            invocations[interopResult.Key] = interopResultValue;
        }

        try
        {
            await JSRuntime.InvokeAsync<object>("jsInteropTests.asyncFunctionThrowsSyncException");
        }
        catch (JSException e)
        {
            SyncExceptionFromAsyncMethod = e;
        }

        try
        {
            await JSRuntime.InvokeAsync<object>("jsInteropTests.asyncFunctionThrowsAsyncException");
        }
        catch (JSException e)
        {
            AsyncExceptionFromAsyncMethod = e;
        }

        var passDotNetObjectByRef = new TestDTO(99999);
        var passDotNetObjectByRefArg = new PassDotNetObjectByRefArgs
        {
            StringValue = "My string",
            TestDto = DotNetObjectReference.Create(passDotNetObjectByRef),
        };
        var result = await JSRuntime.InvokeAsync<ReceiveDotNetObjectByRefArgs>("receiveDotNetObjectByRefAsync", passDotNetObjectByRefArg);
        ReceiveDotNetObjectByRefAsyncResult["stringValueUpper"] = result.StringValueUpper;
        ReceiveDotNetObjectByRefAsyncResult["testDtoNonSerializedValue"] = result.TestDtoNonSerializedValue;
        ReceiveDotNetObjectByRefAsyncResult["testDto"] = result.TestDto.Value == passDotNetObjectByRef ? "Same" : "Different";

        ReturnValues["returnPrimitiveAsync"] = (await JSRuntime.InvokeAsync<int>("returnPrimitiveAsync")).ToString();
        ReturnValues["returnArrayAsync"] = string.Join(",", (await JSRuntime.InvokeAsync<Segment[]>("returnArrayAsync")).Select(x => x.Source).ToArray());
        if (shouldSupportSyncInterop)
        {
            ReturnValues["returnPrimitive"] = ((IJSInProcessRuntime)JSRuntime).Invoke<int>("returnPrimitive").ToString();
            ReturnValues["returnArray"] = string.Join(",", ((IJSInProcessRuntime)JSRuntime).Invoke<Segment[]>("returnArray").Select(x => x.Source).ToArray());
        }

        try
        {
            // Trigger a non-serializable WindowProxy (https://developer.mozilla.org/en-US/docs/Web/API/Window)
            // InvokeVoidAsync shouldn't serialize return values, and thus there shouldn't be any exception thrown.
            await JSRuntime.InvokeVoidAsync("eval", "window");
            ReturnValues["invokeVoidAsyncReturnsWithoutSerializing"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeVoidAsyncReturnsWithoutSerializing"] = $"Failure: {ex.Message}";
        }

        try
        {
            // Trigger a non-serializable WindowProxy (https://developer.mozilla.org/en-US/docs/Web/API/Window)
            // InvokeAsync should serialize return values, and thus there should be an exception thrown.
            var circularStructure = await JSRuntime.InvokeAsync<object>("eval", "window");
            ReturnValues["invokeAsyncThrowsSerializingCircularStructure"] = circularStructure is null ? "Failure: null" : "Failure: not null";
        }
        catch (JSException)
        {
            ReturnValues["invokeAsyncThrowsSerializingCircularStructure"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeAsyncThrowsSerializingCircularStructure"] = $"Failure: {ex.Message}";
        }

        try
        {
            var undefinedJsObjectReference = await JSRuntime.InvokeAsync<IJSObjectReference>("returnUndefined");
            ReturnValues["invokeAsyncThrowsUndefinedJSObjectReference"] = undefinedJsObjectReference is null ? "Failure: null" : "Failure: not null";
        }
        catch (JSException)
        {
            ReturnValues["invokeAsyncThrowsUndefinedJSObjectReference"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeAsyncThrowsUndefinedJSObjectReference"] = $"Failure: {ex.Message}";
        }

        try
        {
            var nullJsObjectReference = await JSRuntime.InvokeAsync<IJSObjectReference>("returnNull");
            ReturnValues["invokeAsyncThrowsNullJSObjectReference"] = nullJsObjectReference is null ? "Failure: null" : "Failure: not null";
        }
        catch (JSException)
        {
            ReturnValues["invokeAsyncThrowsNullJSObjectReference"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeAsyncThrowsNullJSObjectReference"] = $"Failure: {ex.Message}";
        }

        var jsObjectReference = await JSRuntime.InvokeAsync<IJSObjectReference>("returnJSObjectReference");
        ReturnValues["jsObjectReference.identity"] = await jsObjectReference.InvokeAsync<string>("identity", "Invoked from JSObjectReference");
        ReturnValues["jsObjectReference.nested.add"] = (await jsObjectReference.InvokeAsync<int>("nested.add", 2, 3)).ToString();
        ReturnValues["addViaJSObjectReference"] = (await JSRuntime.InvokeAsync<int>("addViaJSObjectReference", jsObjectReference, 2, 3)).ToString();

        try
        {
            // Fetch a non-serializable Window (https://developer.mozilla.org/en-US/docs/Web/API/Window)
            // InvokeVoidAsync shouldn't serialize return values, and thus there shouldn't be any exception thrown.
            await jsObjectReference.InvokeVoidAsync("getWindow");
            ReturnValues["invokeVoidAsyncReturnsWithoutSerializingInJSObjectReference"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeVoidAsyncReturnsWithoutSerializingInJSObjectReference"] = $"Failure: {ex.Message}";
        }

        try
        {
            await jsObjectReference.InvokeAsync<object>("nonFunction");
        }
        catch (JSException e)
        {
            JSObjectReferenceInvokeNonFunctionException = e;
        }

        try
        {
            await jsObjectReference.DisposeAsync();
            ReturnValues["disposeJSObjectReferenceAsync"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["disposeJSObjectReferenceAsync"] = $"Failure: {ex.Message}";
        }

        var module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/testmodule.js");
        ReturnValues["jsObjectReferenceModule"] = await module.InvokeAsync<string>("identity", "Returned from module!");

        if (shouldSupportSyncInterop)
        {
            InvokeInProcessJSInterop();
        }

        var byteArray = new byte[] { 1, 5, 7, 15, 35, 200 };
        var byteArrayWrapperObject = new ByteArrayInterop.ByteArrayWrapper()
        {
            StrVal = "Some String",
            ByteArrayVal = byteArray,
            IntVal = 100000
        };
        ReturnValues["roundTripByteArrayAsyncFromDotNet"] = string.Join(",", (await JSRuntime.InvokeAsync<byte[]>("roundTripByteArrayAsync", byteArray)).ToArray());
        ReturnValues["roundTripByteArrayWrapperObjectAsyncFromDotNet"] = (await JSRuntime.InvokeAsync<ByteArrayInterop.ByteArrayWrapper>("roundTripByteArrayWrapperObjectAsync", byteArrayWrapperObject)).ToString();

        if (shouldSupportSyncInterop)
        {
            ReturnValues["roundTripByteArrayFromDotNet"] = string.Join(",", ((IJSInProcessRuntime)JSRuntime).Invoke<byte[]>("roundTripByteArray", byteArray).ToArray());
            ReturnValues["roundTripByteArrayWrapperObjectFromDotNet"] = ((IJSInProcessRuntime)JSRuntime).Invoke<ByteArrayInterop.ByteArrayWrapper>("roundTripByteArrayWrapperObject", byteArrayWrapperObject).ToString();
        }

        var dataReference = await JSRuntime.InvokeAsync<IJSStreamReference>("jsToDotNetStreamReturnValueAsync");
        using var dataReferenceStream = await dataReference.OpenReadStreamAsync();
        await ValidateStreamValuesAsync("jsToDotNetStreamReturnValueAsync", dataReferenceStream);

        var dataWrapperReference = await JSRuntime.InvokeAsync<JSStreamReferenceWrapper>("jsToDotNetStreamWrapperObjectReturnValueAsync");
        if (dataWrapperReference.StrVal != "SomeStr")
        {
            ReturnValues["jsToDotNetStreamWrapperObjectReturnValueAsync"] = $"StrVal did not match expected 'SomeStr', received {dataWrapperReference.StrVal}.";
        }
        else if (dataWrapperReference.IntVal != 5)
        {
            ReturnValues["jsToDotNetStreamWrapperObjectReturnValueAsync"] = $"IntVal did not match expected '5', received {dataWrapperReference.IntVal}.";
        }
        else
        {
            using var dataWrapperReferenceStream = await dataWrapperReference.JSStreamReferenceVal.OpenReadStreamAsync();
            await ValidateStreamValuesAsync("jsToDotNetStreamWrapperObjectReturnValueAsync", dataWrapperReferenceStream);
        }

        var dotNetStreamReference = DotNetStreamReferenceInterop.GetDotNetStreamReference();
        ReturnValues["dotNetToJSReceiveDotNetStreamReferenceAsync"] = await JSRuntime.InvokeAsync<string>("jsInteropTests.receiveDotNetStreamReference", dotNetStreamReference);

        var dotNetStreamReferenceWrapper = DotNetStreamReferenceInterop.GetDotNetStreamWrapperReference();
        ReturnValues["dotNetToJSReceiveDotNetStreamWrapperReferenceAsync"] = await JSRuntime.InvokeAsync<string>("jsInteropTests.receiveDotNetStreamWrapperReference", dotNetStreamReferenceWrapper);

        await GetValueAsyncTests();

        if (shouldSupportSyncInterop)
        {
            GetValueSyncTests();
        }

        await SetValueAsyncTests();

        if (shouldSupportSyncInterop)
        {
            SetValueTests();
        }

        await InvokeNewAsyncTests();

        if (shouldSupportSyncInterop)
        {
            InvokeNewTests();
        }

        await FunctionReferenceAsyncTests();

        if (shouldSupportSyncInterop)
        {
            FunctionReferenceTests();
        }

        Invocations = invocations;
        DoneWithInterop = true;
    }

    private async Task ValidateStreamValuesAsync(string testName, Stream stream)
    {
        using var memoryStream = new System.IO.MemoryStream();
        await stream.CopyToAsync(memoryStream);
        var buffer = memoryStream.ToArray();

        for (var i = 0; i < buffer.Length; i++)
        {
            var expectedValue = i % 256;
            if (buffer[i] != expectedValue)
            {
                ReturnValues[testName] = $"Failure at index {i}.";
                break;
            }
        }

        if (buffer.Length != 100_000)
        {
            ReturnValues[testName] = $"Failure, got a stream of length {buffer.Length}, expected a length of 100,000.";
        }

        // Mark the test as successful if we haven't already set a failure above
        ReturnValues.TryAdd(testName, "Success");
    }

    public void InvokeInProcessInterop()
    {
        var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);

        try
        {
            inProcRuntime.Invoke<object>("jsInteropTests.functionThrowsException");
        }
        catch (JSException e)
        {
            ExceptionFromSyncMethod = e;
        }

        var passDotNetObjectByRef = new TestDTO(99999);
        var passDotNetObjectByRefArg = new PassDotNetObjectByRefArgs
        {
            StringValue = "My string",
            TestDto = DotNetObjectReference.Create(passDotNetObjectByRef),
        };
        var result = inProcRuntime.Invoke<ReceiveDotNetObjectByRefArgs>("receiveDotNetObjectByRef", passDotNetObjectByRefArg);
        ReceiveDotNetObjectByRefResult["stringValueUpper"] = result.StringValueUpper;
        ReceiveDotNetObjectByRefResult["testDtoNonSerializedValue"] = result.TestDtoNonSerializedValue;
        ReceiveDotNetObjectByRefResult["testDto"] = result.TestDto.Value == passDotNetObjectByRef ? "Same" : "Different";
    }

    public void InvokeInProcessJSInterop()
    {
        var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);

        try
        {
            // Fetch a non-serializable Window (https://developer.mozilla.org/en-US/docs/Web/API/Window)
            // InvokeVoid shouldn't serialize return values, and thus there shouldn't be any exception thrown.
            inProcRuntime.InvokeVoid("eval", "window");
            ReturnValues["invokeVoidReturnsWithoutSerializingIJSInProcessRuntime"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeVoidReturnsWithoutSerializingIJSInProcessRuntime"] = $"Failure: {ex.Message}";
        }

        try
        {
            // Trigger a non-serializable WindowProxy (https://developer.mozilla.org/en-US/docs/Web/API/Window)
            // Invoke should serialize return values, and thus there should be an exception thrown.
            var circularStructure = inProcRuntime.Invoke<object>("eval", "window");
            ReturnValues["invokeThrowsSerializingCircularStructure"] = circularStructure is null ? "Failure: null" : "Failure: not null";
        }
        catch (JSException)
        {
            ReturnValues["invokeThrowsSerializingCircularStructure"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeThrowsSerializingCircularStructure"] = $"Failure: {ex.Message}";
        }

        try
        {
            var undefinedJsObjectReference = inProcRuntime.Invoke<IJSObjectReference>("returnUndefined");
            ReturnValues["invokeThrowsUndefinedJSObjectReference"] = undefinedJsObjectReference is null ? "Failure: null" : "Failure: not null";
        }
        catch (JSException)
        {
            ReturnValues["invokeThrowsUndefinedJSObjectReference"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeThrowsUndefinedJSObjectReference"] = $"Failure: {ex.Message}";
        }

        try
        {
            var nullJsObjectReference = inProcRuntime.Invoke<IJSObjectReference>("returnNull");
            ReturnValues["invokeThrowsNullJSObjectReference"] = nullJsObjectReference is null ? "Failure: null" : "Failure: not null";
        }
        catch (JSException)
        {
            ReturnValues["invokeThrowsNullJSObjectReference"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeThrowsNullJSObjectReference"] = $"Failure: {ex.Message}";
        }

        var jsInProcObjectReference = inProcRuntime.Invoke<IJSInProcessObjectReference>("returnJSObjectReference");
        ReturnValues["jsInProcessObjectReference.identity"] = jsInProcObjectReference.Invoke<string>("identity", "Invoked from JSInProcessObjectReference");

        try
        {
            // Fetch a non-serializable Window (https://developer.mozilla.org/en-US/docs/Web/API/Window)
            // InvokeVoid shouldn't serialize return values, and thus there shouldn't be any exception thrown.
            jsInProcObjectReference.InvokeVoid("getWindow");
            ReturnValues["invokeVoidReturnsWithoutSerializingInIJSInProcessObjectReference"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeVoidReturnsWithoutSerializingInIJSInProcessObjectReference"] = $"Failure: {ex.Message}";
        }
    }

    private async Task GetValueAsyncTests()
    {
        ReturnValues["getValueFromDataPropertyAsync"] = (await JSRuntime.GetValueAsync<int>("jsInteropTests.testObject.num")).ToString();
        ReturnValues["getValueFromGetterAsync"] = (await JSRuntime.GetValueAsync<int>("jsInteropTests.testObject.getOnlyProperty")).ToString();

        try
        {
            var _ = await JSRuntime.GetValueAsync<int>("jsInteropTests.testObject.setOnlyProperty");
        }
        catch (JSException)
        {
            ReturnValues["getValueFromSetterAsync"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["getValueFromSetterAsync"] = $"Failure: {ex.Message}";
        }

        try
        {
            var _ = await JSRuntime.GetValueAsync<int>("jsInteropTests.testObject.undefinedProperty");
        }
        catch (JSException)
        {
            ReturnValues["getValueFromUndefinedPropertyAsync"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["getValueFromUndefinedPropertyAsync"] = $"Failure: {ex.Message}";
        }
    }

    private void GetValueSyncTests()
    {
        var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);

        ReturnValues["getValueFromDataProperty"] = inProcRuntime.GetValue<int>("jsInteropTests.testObject.num").ToString();
        ReturnValues["getValueFromGetter"] = inProcRuntime.GetValue<int>("jsInteropTests.testObject.getOnlyProperty").ToString();

        try
        {
            var _ = inProcRuntime.GetValue<int>("jsInteropTests.testObject.setOnlyProperty");
        }
        catch (JSException)
        {
            ReturnValues["getValueFromSetter"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["getValueFromSetter"] = $"Failure: {ex.Message}";
        }

        try
        {
            var _ = inProcRuntime.GetValue<int>("jsInteropTests.testObject.undefinedProperty");
        }
        catch (JSException)
        {
            ReturnValues["getValueFromUndefinedProperty"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["getValueFromUndefinedProperty"] = $"Failure: {ex.Message}";
        }
    }

    private async Task SetValueAsyncTests()
    {
        ReturnValues["getValueFromGetterAsync"] = (await JSRuntime.GetValueAsync<int>("jsInteropTests.testObject.getOnlyProperty")).ToString();

        await JSRuntime.SetValueAsync("jsInteropTests.testObject.num", 30);
        ReturnValues["setValueToDataPropertyAsync"] = (await JSRuntime.GetValueAsync<int>("jsInteropTests.testObject.num")).ToString();

        await JSRuntime.SetValueAsync("jsInteropTests.testObject.setOnlyProperty", 40);
        ReturnValues["setValueToSetterAsync"] = (await JSRuntime.GetValueAsync<int>("jsInteropTests.testObject.num")).ToString();

        await JSRuntime.SetValueAsync("jsInteropTests.testObject.newProperty", 50);
        ReturnValues["setValueToUndefinedPropertyAsync"] = (await JSRuntime.GetValueAsync<int>("jsInteropTests.testObject.newProperty")).ToString();

        try
        {
            await JSRuntime.SetValueAsync("jsInteropTests.testObject.getOnlyProperty", 50);
        }
        catch (JSException)
        {
            ReturnValues["setValueToGetterAsync"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["setValueToGetterAsync"] = $"Failure: {ex.Message}";
        }
    }

    private void SetValueTests()
    {
        var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);

        ReturnValues["getValueFromGetter"] = inProcRuntime.GetValue<int>("jsInteropTests.testObject.getOnlyProperty").ToString();

        inProcRuntime.SetValue("jsInteropTests.testObject.num", 30);
        ReturnValues["setValueToDataProperty"] = inProcRuntime.GetValue<int>("jsInteropTests.testObject.num").ToString();

        inProcRuntime.SetValue("jsInteropTests.testObject.setOnlyProperty", 40);
        ReturnValues["setValueToSetter"] = inProcRuntime.GetValue<int>("jsInteropTests.testObject.num").ToString();

        inProcRuntime.SetValue("jsInteropTests.testObject.newProperty", 50);
        ReturnValues["setValueToUndefinedProperty"] = inProcRuntime.GetValue<int>("jsInteropTests.testObject.newProperty").ToString();

        try
        {
            inProcRuntime.SetValue("jsInteropTests.testObject.getOnlyProperty", 60);
        }
        catch (JSException)
        {
            ReturnValues["setValueToGetter"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["setValueToGetter"] = $"Failure: {ex.Message}";
        }
    }

    private async Task InvokeNewAsyncTests()
    {
        var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka");

        ReturnValues["invokeNewWithClassConstructorAsync"] = testClassRef is IJSObjectReference ? "Success" : "Failure";
        ReturnValues["invokeNewWithClassConstructorAsync.dataProperty"] = await testClassRef.GetValueAsync<string>("text");
        ReturnValues["invokeNewWithClassConstructorAsync.function"] = (await testClassRef.InvokeAsync<int>("getTextLength")).ToString();

        try
        {
            var nonConstructorRef = await JSRuntime.InvokeNewAsync("jsInteropTests.nonConstructorFunction");
            ReturnValues["invokeNewWithNonConstructorAsync"] = nonConstructorRef is null ? "Failure: null" : "Failure: not null";
        }
        catch (JSException)
        {
            ReturnValues["invokeNewWithNonConstructorAsync"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeNewWithNonConstructorAsync"] = $"Failure: {ex.Message}";
        }
    }

    private void InvokeNewTests()
    {
        var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);

        var testClassRef = inProcRuntime.InvokeNew("jsInteropTests.TestClass", "abraka");

        ReturnValues["invokeNewWithClassConstructor"] = testClassRef is IJSInProcessObjectReference ? "Success" : "Failure";
        ReturnValues["invokeNewWithClassConstructor.dataProperty"] = testClassRef.GetValue<string>("text");
        ReturnValues["invokeNewWithClassConstructor.function"] = testClassRef.Invoke<int>("getTextLength").ToString();

        try
        {
            var nonConstructorRef = inProcRuntime.InvokeNew("jsInteropTests.nonConstructorFunction");
            ReturnValues["invokeNewWithNonConstructor"] = nonConstructorRef is null ? "Failure: null" : "Failure: not null";
        }
        catch (JSException)
        {
            ReturnValues["invokeNewWithNonConstructor"] = "Success";
        }
        catch (Exception ex)
        {
            ReturnValues["invokeNewWithNonConstructor"] = $"Failure: {ex.Message}";
        }
    }

    private async Task FunctionReferenceAsyncTests()
    {
        var funcRef = await JSRuntime.GetValueAsync<IJSObjectReference>("jsInteropTests.nonConstructorFunction");
        var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka");
        await testClassRef.SetValueAsync("getTextLength", funcRef);
        ReturnValues["changeFunctionViaObjectReferenceAsync"] = (await testClassRef.InvokeAsync<int>("getTextLength")).ToString();
    }

    private void FunctionReferenceTests()
    {
        var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);

        var funcRef = inProcRuntime.GetValue<IJSObjectReference>("jsInteropTests.nonConstructorFunction");
        var testClassRef = inProcRuntime.InvokeNew("jsInteropTests.TestClass", "abraka");
        testClassRef.SetValue("getTextLength", funcRef);
        ReturnValues["changeFunctionViaObjectReference"] = testClassRef.Invoke<int>("getTextLength").ToString();
    }

    public class PassDotNetObjectByRefArgs
    {
        public string StringValue { get; set; }

        public DotNetObjectReference<TestDTO> TestDto { get; set; }
    }

    public class ReceiveDotNetObjectByRefArgs
    {
        public string StringValueUpper { get; set; }

        public int TestDtoNonSerializedValue { get; set; }

        public DotNetObjectReference<TestDTO> TestDto { get; set; }
    }
}
